package com.investmenttechnologyservices.services;

import static com.google.common.base.Preconditions.checkNotNull;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
import com.google.appengine.api.datastore.Query.Filter;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.appengine.api.datastore.Query.SortDirection;
import com.google.inject.Inject;
import com.investmenttechnologyservices.datadictionary.FieldType;
import com.investmenttechnologyservices.model.Instrument;
import com.investmenttechnologyservices.model.InstrumentType;
import com.investmenttechnologyservices.model.Position;
import com.investmenttechnologyservices.model.PositionType;
import com.investmenttechnologyservices.model.PriceFormat;
import com.investmenttechnologyservices.model.StageType;
import com.investmenttechnologyservices.model.Symbol;
import com.investmenttechnologyservices.model.Trade;
import com.investmenttechnologyservices.util.DateUtil;


public class PositionServiceImpl implements PositionService {
	/**
	 * getPositionQuantityAsOf
	 * Returns a list of positions in the given account as of a date for a list of symbols. If no symbols are 
	 * listed (symbolKeys == null) then all the positions in the account are returned.
	 *  
	 * @param ds
	 * @param asOfDate
	 * @param accountKey
	 * @param symbolKeys
	 * @param tradesOnly
	 * @return List<Position>
	 */
	private static final Logger log = Logger.getLogger(PositionServiceImpl.class.getName());
	private static final Key itsKey = KeyFactory.createKey("dataProvider", "ITS");

	private static final String POSITION = "position";
	
	private DatastoreService ds;
	private AccountService accountService;
	private InstrumentService instrumentService;
	private SymbolService symbolService; 
	private LoginService loginService;
	private DatastoreHelper datastoreHelper;
	
	@Inject
	PositionServiceImpl(DatastoreService ds, 
			AccountService accountService, 
			InstrumentService instrumentService,
			SymbolService symbolService,
			LoginService loginService,
			DatastoreHelper datastoreHelper) {
		this.ds = ds;
		this.accountService = accountService;
		this.instrumentService = instrumentService;
		this.symbolService = symbolService;
		this.loginService = loginService;
		this.datastoreHelper = datastoreHelper;
	}
	
	private class MapObject{
		public InstrumentType instrumentType;
		public PriceFormat priceFormat;
		public List<Entity> entities;

/*		public void setInstrumentType(InstrumentType instrumentType) {this.instrumentType = instrumentType;}
		public void setPriceFormat(PriceFormat priceFormat) {this.priceFormat = priceFormat;}
		public void setEntities(List<Entity> entities) {this.entities = entities;}
		public InstrumentType getInstrumentType() {return instrumentType;}
		public PriceFormat getPriceFormat() {return priceFormat;}
		public List<Entity> getEntities() {return entities;}
*/
	}
	private MapObject mapObject = null;
	private void setMapObject(InstrumentType instrumentType, PriceFormat priceFormat, List<Entity> entities) {
		mapObject = new MapObject();
		mapObject.instrumentType = instrumentType;
		mapObject.priceFormat = priceFormat;
		mapObject.entities = entities;
	}

	@Override
	public void findAll(HttpServletRequest req, HttpServletResponse res) throws Exception {
		
		if (accountService.checkUserAndAccount(null)) {
			
			Key firmKey = loginService.getFirmKeyForCurrentUser();

			@SuppressWarnings("unchecked")
			List<Long> accountList = (List<Long>) FieldType.ARRAYLONG.stringToValue(req.getParameter("accountIds"));

			List<Key> accountKeys = new ArrayList<Key>();
			for (Long accountId : accountList) {
				accountKeys.add(KeyFactory.createKey(firmKey, "account", accountId));
			}
			
			if (accountKeys.size()>0) {
				List<Position> positions = getMultipleAccountsPositionsAsOf(accountKeys);
				List<Entity> entities = new ArrayList<Entity>();
				for (Position position : positions) {
					entities.add(position.asEntity());
				}
				datastoreHelper.writeEntities(res.getWriter(), POSITION, entities);
			}
		}
	}

	
	/**
	 * getSymbolPositionsAsOf
	 * Returns a list of positions in the given account as of a date for a list of symbols. If no symbols are 
	 * listed (symbolKeys == null) then all the positions in the account are returned. A new DatastoreService is
	 * defined.
	 *
	 * @param asOfDate
	 * @param accountKey
	 * @param symbolKeys
	 * @param tradesOnly
	 * @return List<Position>
	 */
	@Override
	public List<Position> getSymbolPositionsAsOf(Date asOfDate, Key accountKey, 
			List<Key> symbolKeys, Boolean tradesOnly) {
		checkNotNull(accountKey, "Account required in order to retrieve positions!");
		
		List<Position> mapSymbolQuantity = null;
		
		if (asOfDate == null) {
			asOfDate = new Date();
		}
		
		if (symbolKeys == null) {
			symbolKeys = new ArrayList<Key>();
		} else {
			Collections.sort(symbolKeys);
		}

		Map<Key, Date> mapDates = getDateZeroPositionAccount(accountKey, symbolKeys, tradesOnly, asOfDate);

		// Step 2: with list of dates that positions were initiated get all the subsequent trades
		Map<Key, MapObject> mapSymbolTrades = getTradesToBuildPositionAsOf(asOfDate, 
				accountKey, mapDates, tradesOnly);		

		mapSymbolQuantity = getSymbolQuantityAsOfDate(asOfDate, mapSymbolTrades);
		
		return mapSymbolQuantity;
	}
	/**
	 * getPositionQuantityAsOf
	 * Returns a list of positions in the given account as of today. 
	 * All the positions in the account are returned. 
	 *
	 * @param accountKey
	 * @return List<Position>
	 */
	@Override
	public List<Position> getCurrentAccountPositions(Key accountKey) {
		return getSymbolPositionsAsOf(null, accountKey, null, false);
	}
	
	@Override 
	public List<Position> getAccountPositionsAsOf(Key accountKey, Date asOfDate) {
		return getSymbolPositionsAsOf(asOfDate, accountKey, null, false);
	}
	
	@Override
	public List<Position> getMultipleAccountsPositionsAsOf(List<Key> accountKeys){
		List<Position> positions = new ArrayList<Position>();
		if (accountKeys.isEmpty())
			return null;
		else {
			for(Key account : accountKeys) {
				positions.addAll(getCurrentAccountPositions(account));
			}
			return positions;
		}
	}

	
	private Map<Key, Date> getDateZeroPositionAccount(Key accountKey, List<Key> symbolKeys, 
			Boolean tradesOnly, Date asOfDate) {
		
		log.info("getDateZeroPositionAccount(in): ds="+ds.toString()+", accountKey="+accountKey.toString()+", symbolKey="+
				symbolKeys.toString()+", tradesOnly="+tradesOnly.toString()+", asOfDate="+asOfDate.toString());
		
		Date lastLoadDate = accountService.getAccountLastLoadedDate(accountKey);
		// Set filters:
		//	1. effective as of the date
		//	2. not expired
		//	3. previous position was zero
		//	4. check stage - include order/execs or not
		//	5. use account level last load date if available
		//	6. specific symbols or not
		FetchOptions fo = FetchOptions.Builder.withDefaults();
		Filter dateFilter =
				new FilterPredicate("effectiveTimestamp",FilterOperator.LESS_THAN_OR_EQUAL,asOfDate);
		Filter tradeExpirationFilter = 
				new FilterPredicate("expirationTimestamp",FilterOperator.EQUAL,null);
		Filter qtyPrevNullFilter =
				new FilterPredicate("quantityPrevious",FilterOperator.EQUAL,null);
		Filter notActiveFilter =
				CompositeFilterOperator.and(dateFilter, tradeExpirationFilter, qtyPrevNullFilter);
		Filter tradeStageTypeFilter;
		if (tradesOnly){
			tradeStageTypeFilter = 
					new FilterPredicate("stageType",FilterOperator.EQUAL,StageType.TRANSACTION.name());
		} else {
			tradeStageTypeFilter = CompositeFilterOperator.or(
					new FilterPredicate("stageType",FilterOperator.EQUAL,StageType.ORDER.name()),
					new FilterPredicate("stageType",FilterOperator.EQUAL,StageType.EXECUTION.name()),
					new FilterPredicate("stageType",FilterOperator.EQUAL,StageType.TRANSACTION.name()));
		}
		notActiveFilter =
				CompositeFilterOperator.and(notActiveFilter,tradeStageTypeFilter);
		if (lastLoadDate.before(asOfDate)) {
			dateFilter =
					new FilterPredicate("effectiveTimestamp",FilterOperator.GREATER_THAN_OR_EQUAL,lastLoadDate);
			notActiveFilter =
					CompositeFilterOperator.and(dateFilter,notActiveFilter);
		}

		Query query;
		
		if (symbolKeys.isEmpty()) {
			query = new Query ("trade").setFilter(notActiveFilter)
					.setAncestor(accountKey)
					.addSort("effectiveTimestamp", SortDirection.DESCENDING);
		} else {
			Filter symbolFilter =
					new FilterPredicate("symbolKey",FilterOperator.IN,symbolKeys);
			notActiveFilter =
					CompositeFilterOperator.and(notActiveFilter, symbolFilter);

			query =new Query ("trade").setFilter(notActiveFilter)
						.setAncestor(accountKey)
						.addSort("effectiveTimestamp", SortDirection.DESCENDING);
		}


		List<Entity> result = ds.prepare(query).asList(fo);
		log.info(result.size()+" records found for account "+accountKey.toString());

		Map<Key, Date> positionOpenDates = new TreeMap<Key, Date>();

		if (!result.isEmpty()) {
			// resulting query is sorted by effectiveTimestamp in descending order
			Map<Key,List<Entity>> sortedResults = new HashMap<Key, List<Entity>>();
			Iterator<Entity> iter = result.iterator();
			Entity entity;
			Trade trade;
			List<Entity> trades;
			// iterate over the results and group by symbolKey
			while (iter.hasNext()) {
				entity = iter.next();
				trade = new Trade(entity);
				// get Map entry for the symbol in trade
				trades = sortedResults.get(trade.getSymbolKey());
				if (trades == null) {
					trades = new ArrayList<Entity>();
				}
				trades.add(entity);
				sortedResults.put(trade.getSymbolKey(),trades);
			}

			// iterate over Map now keyed by Symbol
			Iterator<Entry<Key, List<Entity>>> it = sortedResults.entrySet().iterator();
		    Map.Entry<Key,List<Entity>> pair;
		    Key symbolKey;
		    Date effectiveTimestamp;
	        Date newPositionDate;
		    while (it.hasNext()) {
		        pair = (Entry<Key, List<Entity>>)it.next();
		        symbolKey = pair.getKey();
		        trades = pair.getValue();
		        newPositionDate = null;
		        // iterate over List of trades
		        if (!trades.isEmpty()) {
					iter = trades.iterator();
					while (iter.hasNext()) {
						entity = iter.next();
						effectiveTimestamp = new Trade(entity).getEffectiveTimestamp();
						if (newPositionDate == null) {
							newPositionDate = effectiveTimestamp;
						} else if (newPositionDate.before(effectiveTimestamp)) {
							newPositionDate = effectiveTimestamp;
						}
					
					}
					// add Symbol and date to list to be returned from method
					if (newPositionDate != null) {
						positionOpenDates.put(symbolKey, newPositionDate);
					}
				}
		    }
		} else {
			log.info("No initial positions found. Account= "+accountKey.toString());
		}
		log.info("getDateZeroPositionAccount(out): positionOpenDate.size="+positionOpenDates.size());
		return positionOpenDates;

	}

	private Map<Key, MapObject> getTradesToBuildPositionAsOf(Date asOfDate, Key accountKey, 
			Map<Key, Date> mapDates, Boolean tradesOnly) {

		log.info("getSymbolPositionQuantityAsOfDate(in): ds="+ds.toString()+", asOfDate="+asOfDate.toString()+", accountKey="+accountKey.toString()+
				", tradesOnly="+tradesOnly.toString());
		
		Key symbolKey, instrumentKey;
		Symbol symbol;
		Instrument instrument;
		InstrumentType instrumentType;
		String strCurrency;
		
		Map<Key,MapObject> mapTrades = new TreeMap<Key,MapObject>();
		// Set up query filters. First set common filters
		//	1. stageType
		//	2. effectiveTimestamp - relevant to period
		//	3. expirationTimestamp - still active trade
		FetchOptions fo = FetchOptions.Builder.withDefaults();
		Filter tradeStageTypeFilter;
		if (tradesOnly){
			tradeStageTypeFilter = 
					new FilterPredicate("stageType",FilterOperator.EQUAL,StageType.TRANSACTION.name());
		} else {
			tradeStageTypeFilter = CompositeFilterOperator.or(
					new FilterPredicate("stageType",FilterOperator.EQUAL,StageType.ORDER.name()),
					new FilterPredicate("stageType",FilterOperator.EQUAL,StageType.EXECUTION.name()),
					new FilterPredicate("stageType",FilterOperator.EQUAL,StageType.TRANSACTION.name()));
		}
		Filter constantFilter =
			CompositeFilterOperator.and(
				new FilterPredicate("effectiveTimestamp",FilterOperator.LESS_THAN_OR_EQUAL,asOfDate), 
				new FilterPredicate("expirationTimestamp",FilterOperator.EQUAL,null),
				tradeStageTypeFilter);

		Query query;
		Filter notActiveFilter;
		PriceFormat priceFormat;
		
		// iterate on list of Symbols and the date on which the position was first initialized
		for (Map.Entry<Key, Date> entry : mapDates.entrySet()) {
			log.info("[mapDates(1): key="+entry.getKey().toString()+", value="+entry.getValue().toString());
			// set up iteration specific filters
			//	4. look for trades following initialization of position
			//	5. Symbol specific

			symbolKey = entry.getKey();

			List<Entity> settledCash = new ArrayList<Entity>();
			List<Entity> unsettledCash = new ArrayList<Entity>();
			Trade trade;

			notActiveFilter =
				CompositeFilterOperator.and(
					new FilterPredicate("effectiveTimestamp",FilterOperator.GREATER_THAN_OR_EQUAL,entry.getValue()),
					constantFilter,
					new FilterPredicate("symbolKey",FilterOperator.EQUAL,symbolKey));
		
			query = 
				new Query ("trade").setFilter(notActiveFilter)
					.setAncestor(accountKey)
					.addSort("effectiveTimestamp", SortDirection.ASCENDING)
					.addSort("stageType", SortDirection.ASCENDING);
			List<Entity> result = ds.prepare(query).asList(fo);
			
			// Special cash handling - move trades that haven't settled to unsettled cash instrument
			// add Symbol (as key) and trades to return value
			if (!result.isEmpty()) {
				try {
					instrument = new Instrument(ds.get(symbolKey.getParent()));
					priceFormat = symbolService.getPriceFormat(symbolKey);
					instrumentType = instrument.getInstrumentType();
					
					if (instrumentType==InstrumentType.CASH) {
						symbol = new Symbol(ds.get(symbolKey));
						instrumentKey = instrumentService.getCashInstrument(symbol.getCurrency(), 
								InstrumentType.CASH_UNST);
						strCurrency = symbol.getCurrency().getCurrencyCode();
						symbolKey = symbolService.getSymbol(instrumentKey, itsKey);
						if (symbolKey==null) 
							symbolKey = symbolService.createSymbolITS(instrumentKey, 
									strCurrency, 
									instrumentKey, 
									symbol.getCurrency());
						
						//Loop through trades and determine which have settled.
						for (Entity entity : result) {
							trade = new Trade(entity);
							if (trade.getSettlementDate()!= null &&
								trade.getSettlementDate().after(asOfDate))
								unsettledCash.add(entity);
							else
								settledCash.add(entity);
						}
						if (!settledCash.isEmpty()) {
							setMapObject(instrumentType, priceFormat, settledCash);
							mapTrades.put(entry.getKey(), mapObject);
						}
						if (!unsettledCash.isEmpty()) {
							setMapObject(instrumentType, priceFormat, unsettledCash);
							mapTrades.put(symbolKey, mapObject);
						}
					} else {
						setMapObject(instrumentType, priceFormat, result);
						mapTrades.put(symbolKey, mapObject);
					}
				} catch (EntityNotFoundException ex) {
					// TODO Auto-generated catch block
					ex.printStackTrace();
				}
			} else {
				log.info("No records found!");
			}
		}

		log.info("getSymbolPositionQuantityAsOfDate(out): mapTrades(size)="+mapTrades.size());
		return mapTrades;
	}

	private List<Position> getSymbolQuantityAsOfDate(Date asOfDate, Map<Key,MapObject> mapSymbolTrades){
		
		log.info("getSymbolQuantityAsOfDate(in): mapSymbolTrades.size="+mapSymbolTrades.size());

		Key symbolKey, tradeKey;
		Boolean cashPosition;
		Date settlementDate;
		InstrumentType instrumentType;
		PriceFormat priceFormat;

		StageType stageType;
		Trade trade;
		
		List<Entity> trades = new ArrayList<Entity>();
		
		List<Position> positions = new ArrayList<Position>();
		
		for (Map.Entry<Key, MapObject> entry : mapSymbolTrades.entrySet()) {

			Double quantity = 0.0;
			Key instrumentKey;
			Entity instrumentEntity;
			Double underlyingQuantity;
			Key underlyingSymbolKey;
		
			symbolKey 		= entry.getKey();
			trades 			= entry.getValue().entities;
			instrumentType 	= entry.getValue().instrumentType;
			priceFormat	 	= entry.getValue().priceFormat;

			for (Entity entity : trades) {
				trade = new Trade(entity);
				quantity = trade.getQuantity();
				stageType = trade.getStageType();
				// cash order/simulation
				cashPosition = (trade.getInstrumentTradeKey()!=null)?true:false;
				tradeKey = cashPosition?trade.getInstrumentTradeKey():trade.getKey();
				
				if (stageType==StageType.TRANSACTION &&
					trade.getSettlementDate() != null)
					settlementDate = (trade.getSettlementDate().before(asOfDate))?
						DateUtil.dateNowUTC():trade.getSettlementDate();
				else
					settlementDate = null;
				
				positions.add(new Position(trade.getParentKey(), symbolKey, quantity, PositionType.HELD, stageType, 
					settlementDate, trade.getKey(), cashPosition, priceFormat, null,
					instrumentType));
				
				// Look-through?
				//TODO this should be recursive
				if (instrumentType.getUnderlyingExposure()) {
					// get the underlying instrument
					try {
						instrumentKey = symbolKey.getParent();
						instrumentEntity = ds.get(instrumentKey);
						instrumentKey = (Key)instrumentEntity.getProperty("underlyingInstrumentKey");
						underlyingQuantity = (Double)instrumentEntity.getProperty("notionalAmount");
						instrumentType = InstrumentType.valueOf((String)instrumentEntity.getProperty("instrumentType"));
						if (instrumentKey != null && 
							underlyingQuantity != null && 
							underlyingQuantity != 0.0 &&
							instrumentType != null) {
							// Calculate the underlying
							underlyingSymbolKey = symbolService.getSymbol(instrumentKey, itsKey);
							if (underlyingSymbolKey != null) {
								positions.add(new Position(trade.getParentKey(), underlyingSymbolKey, quantity*underlyingQuantity, 
										PositionType.DERIVED, stageType, settlementDate, 
										trade.getKey(), cashPosition, priceFormat, symbolKey,
										instrumentType));
							}
						}
					} catch (EntityNotFoundException ex) {
						// TODO Auto-generated catch block
						ex.printStackTrace();
					}
				}
			}
		}
		log.info("getSymbolQuantityAsOfDate(out): positions.size="+positions.size());
		return positions;
	}

}
